Compare commits

...

7 Commits

Author SHA1 Message Date
louiscklaw
ed95621b2f update demo-react-quotes, 2025-06-08 19:07:48 +08:00
louiscklaw
2258fd8fb9 update demo-react-overlay-hooks, 2025-06-08 19:07:48 +08:00
louiscklaw
0f674badd9 update demo-react-onboarding-ui, 2025-06-08 19:07:48 +08:00
louiscklaw
bc731ea2b8 update demo-react-notes, 2025-06-08 19:07:48 +08:00
louiscklaw
c11dba6297 update demo-react-movie-app-with-algolia, 2025-06-08 19:07:38 +08:00
louiscklaw
b9fbe4e476 update demo-react-marvel-app, 2025-06-08 19:07:38 +08:00
louiscklaw
0d844eed3f update demo-react-login, 2025-06-08 19:07:38 +08:00
80 changed files with 3601 additions and 641 deletions

View File

@@ -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} &rarr;
</IonRouterLink>
</p>
</IonCol>
</IonRow>
);

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View 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 &rarr;
</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;

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 />

View File

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

View File

@@ -15,7 +15,7 @@ import {
useIonRouter,
} from '@ionic/react';
import './Home.css';
import './Home.scss';
import {
addOutline,
chevronBackOutline,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
{/* */}

View File

@@ -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="&nbsp;&nbsp;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;

View File

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

View File

@@ -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="&nbsp;&nbsp;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;

View File

@@ -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 } />&nbsp;&nbsp;Install App
</IonButton>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<CustomInfiniteHits />
</IonContent>
</InstantSearch>
</IonPage>
);
};
export default Movies;

View File

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

View File

@@ -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} />
&nbsp;&nbsp;Install App
</IonButton>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<CustomInfiniteHits />
</IonContent>
</InstantSearch>
</IonPage>
);
};
export default Movies;

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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 &rarr;
</IonButton>
)}
{finalSlide && (
<>
<IonButton expand="block" fill="solid">
Register
</IonButton>
<IonButton expand="block" fill="outline">
Login
</IonButton>
</>
)}
</IonCol>
</IonRow>
</IonGrid>
</IonSlide>
);
};
export default OnboardingSlide;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
ion-menu ion-content {
--background: var(--ion-item-background, var(--ion-background-color, #fff));
}
ion-menu.md ion-content {
--padding-start: 8px;
--padding-end: 8px;
--padding-top: 20px;
--padding-bottom: 20px;
}
ion-menu.md ion-list {
padding: 20px 0;
}
ion-menu.md ion-note {
margin-bottom: 30px;
}
ion-menu.md ion-list-header, ion-menu.md ion-note {
padding-left: 10px;
}
ion-menu.md ion-list#inbox-list {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
ion-menu.md ion-list#inbox-list ion-list-header {
font-size: 22px;
font-weight: 600;
min-height: 20px;
}
ion-menu.md ion-list#labels-list ion-list-header {
font-size: 16px;
margin-bottom: 18px;
color: #757575;
min-height: 26px;
}
ion-menu.md ion-item {
--padding-start: 10px;
--padding-end: 10px;
border-radius: 4px;
}
ion-menu.md ion-item.selected {
--background: rgba(var(--ion-color-primary-rgb), 0.14);
}
ion-menu.md ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.md ion-item ion-icon {
color: #616e7e;
}
ion-menu.md ion-item ion-label {
font-weight: 500;
}
ion-menu.ios ion-content {
--padding-bottom: 20px;
}
ion-menu.ios ion-list {
padding: 20px 0 0 0;
}
ion-menu.ios ion-note {
line-height: 24px;
margin-bottom: 20px;
}
ion-menu.ios ion-item {
--padding-start: 16px;
--padding-end: 16px;
--min-height: 50px;
}
ion-menu.ios ion-item ion-icon {
font-size: 24px;
color: #73849a;
}
ion-menu.ios ion-item .selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.ios ion-list#labels-list ion-list-header {
margin-bottom: 8px;
}
ion-menu.ios ion-list-header,
ion-menu.ios ion-note {
padding-left: 16px;
padding-right: 16px;
}
ion-menu.ios ion-note {
margin-bottom: 8px;
}
ion-note {
display: inline-block;
font-size: 16px;
color: var(--ion-color-medium-shade);
}
ion-item.selected {
--color: var(--ion-color-primary);
}

View File

@@ -0,0 +1,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;

View File

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

View File

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

View File

@@ -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? Its 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
ion-menu ion-content {
--background: var(--ion-item-background, var(--ion-background-color, #fff));
}
ion-menu.md ion-content {
--padding-start: 8px;
--padding-end: 8px;
--padding-top: 20px;
--padding-bottom: 20px;
}
ion-menu.md ion-list {
padding: 20px 0;
}
ion-menu.md ion-note {
margin-bottom: 30px;
}
ion-menu.md ion-list-header, ion-menu.md ion-note {
padding-left: 10px;
}
ion-menu.md ion-list#inbox-list {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
ion-menu.md ion-list#inbox-list ion-list-header {
font-size: 22px;
font-weight: 600;
min-height: 20px;
}
ion-menu.md ion-list#labels-list ion-list-header {
font-size: 16px;
margin-bottom: 18px;
color: #757575;
min-height: 26px;
}
ion-menu.md ion-item {
--padding-start: 10px;
--padding-end: 10px;
border-radius: 4px;
}
ion-menu.md ion-item.selected {
--background: rgba(var(--ion-color-primary-rgb), 0.14);
}
ion-menu.md ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.md ion-item ion-icon {
color: #616e7e;
}
ion-menu.md ion-item ion-label {
font-weight: 500;
}
ion-menu.ios ion-content {
--padding-bottom: 20px;
}
ion-menu.ios ion-list {
padding: 20px 0 0 0;
}
ion-menu.ios ion-note {
line-height: 24px;
margin-bottom: 20px;
}
ion-menu.ios ion-item {
--padding-start: 16px;
--padding-end: 16px;
--min-height: 50px;
}
ion-menu.ios ion-item ion-icon {
font-size: 24px;
color: #73849a;
}
ion-menu.ios ion-item .selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.ios ion-list#labels-list ion-list-header {
margin-bottom: 8px;
}
ion-menu.ios ion-list-header,
ion-menu.ios ion-note {
padding-left: 16px;
padding-right: 16px;
}
ion-menu.ios ion-note {
margin-bottom: 8px;
}
ion-note {
display: inline-block;
font-size: 16px;
color: var(--ion-color-medium-shade);
}
ion-item.selected {
--color: var(--ion-color-primary);
}

View File

@@ -0,0 +1,62 @@
import {
IonContent,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonNote,
} from '@ionic/react';
import { useLocation } from 'react-router-dom';
import { bookmarkOutline, bookmarkSharp, homeOutline, homeSharp } from 'ionicons/icons';
import './Menu.css';
import { useStoreState } from 'pullstate';
import { QuoteStore } from '../store';
import { getSavedQuotes } from '../store/Selectors';
const Menu = () => {
const location = useLocation();
const saved = useStoreState(QuoteStore, getSavedQuotes);
const appPages = [
{
title: 'Home',
url: '/home',
iosIcon: homeOutline,
mdIcon: homeSharp
},
{
title: `Bookmarks (${ saved.length })`,
url: '/saved',
iosIcon: bookmarkOutline,
mdIcon: bookmarkSharp
}
];
return (
<IonMenu contentId="main" type="overlay">
<IonContent>
<IonList id="inbox-list" className="ion-margin-top">
<IonListHeader>Ionic Quotes</IonListHeader>
<IonNote>hey there!</IonNote>
{appPages.map((appPage, index) => {
return (
<IonMenuToggle key={index} autoHide={false}>
<IonItem className={location.pathname === appPage.url ? 'selected' : ''} routerLink={appPage.url} routerDirection="none" lines="none" detail={false}>
<IonIcon slot="start" ios={appPage.iosIcon} md={appPage.mdIcon} />
<IonLabel>{appPage.title}</IonLabel>
</IonItem>
</IonMenuToggle>
);
})}
</IonList>
</IonContent>
</IonMenu>
);
};
export default Menu;

View File

@@ -0,0 +1,21 @@
.quoteItem {
--quote-item-background: rgb(49, 117, 226);
border: 2px solid rgb(154, 204, 245);
border-radius: 10px;
--background: var(--quote-item-background);
background: var(--quote-item-background);
color: white;
padding: 1rem;
}
.quoteText p {
color: rgb(25, 51, 93);
}
.quoteText h1:hover {
color: white;
}

View File

@@ -0,0 +1,17 @@
import { IonCol, IonItem, IonLabel } from "@ionic/react";
import styles from "./QuoteItem.module.css";
export const QuoteItem = ({ quote }) => {
return (
<IonCol size="6" className="animate__animated animate__fadeIn">
<IonItem lines="none" className={ styles.quoteItem } routerLink={ `/quote/${ quote.id }`}>
<IonLabel className={ styles.quoteText }>
<h2>{ quote.text }</h2>
<p>{ quote.author }</p>
</IonLabel>
</IonItem>
</IonCol>
);
}

View File

@@ -0,0 +1,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;

View File

@@ -0,0 +1,88 @@
import { IonBackButton, IonButton, IonButtons, IonCard, IonCardContent, IonCol, IonContent, IonHeader, IonIcon, IonImg, IonPage, IonRow, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
import { bookmarkOutline, checkmarkOutline, copyOutline } from 'ionicons/icons';
import { useStoreState } from 'pullstate';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { QuoteStore } from '../store';
import { addSavedQuote, removeSavedQuote } from '../store/QuoteStore';
import { getQuote, getSavedQuotes } from '../store/Selectors';
import { Clipboard } from '@capacitor/clipboard';
const Quote = () => {
const { id } = useParams();
const quote = useStoreState(QuoteStore, getQuote(id));
const saved = useStoreState(QuoteStore, getSavedQuotes);
const [ bookmarked, setBookmarked ] = useState(false);
const [ present ] = useIonToast();
useEffect(() => {
setBookmarked(saved.includes(parseInt(id)));
}, [ saved, id ]);
const copyQuote = async () => {
await Clipboard.write({
string: quote.text
});
present({
header: "Success",
message: "Quote copied to clipboard!",
duration: 2500,
color: "primary"
});
}
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton text="Home" />
</IonButtons>
<IonTitle>Quote</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Quote</IonTitle>
</IonToolbar>
</IonHeader>
<IonCard className="animate__animated animate__slideInRight animate__faster">
<IonImg src={ quote.image } alt="quote cover" />
<IonCardContent>
<h1>{ quote.text }</h1>
<p>- { quote.author }</p>
</IonCardContent>
<IonRow>
<IonCol size="6">
<IonButton fill={ !bookmarked ? "outline" : "solid" } onClick={ () => bookmarked ? removeSavedQuote(quote.id) : addSavedQuote(quote.id) }>
<IonIcon icon={ bookmarked ? checkmarkOutline : bookmarkOutline } />
&nbsp;{ bookmarked ? "Bookmarked" : "Save as Bookmark" }
</IonButton>
</IonCol>
<IonCol size="4">
<IonButton fill="outline" onClick={ copyQuote }>
<IonIcon icon={ copyOutline } />
&nbsp;Copy Quote
</IonButton>
</IonCol>
</IonRow>
</IonCard>
</IonContent>
</IonPage>
);
};
export default Quote;

View File

@@ -0,0 +1,74 @@
import { IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonMenuButton, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
import { useStoreState } from 'pullstate';
import { useState } from 'react';
import { QuoteItem } from '../components/QuoteItem';
import { QuoteStore } from '../store';
import { getQuotes, getSavedQuotes } from '../store/Selectors';
const Saved = () => {
const quotes = useStoreState(QuoteStore, getQuotes);
const saved = useStoreState(QuoteStore, getSavedQuotes);
const [ amountLoaded, setAmountLoaded ] = useState(20);
const fetchMore = async e => {
setAmountLoaded(amountLoaded => amountLoaded + 20);
e.target.complete();
}
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Bookmarks</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Bookmarks</IonTitle>
</IonToolbar>
</IonHeader>
<IonGrid>
{ quotes.length > 0 &&
<IonList>
<IonRow>
{ quotes.map((quote, index) => {
if ((index <= amountLoaded) && saved.includes(parseInt(quote.id))) {
return (
<QuoteItem key={ index } quote={ quote } />
);
} else return "";
})}
<IonInfiniteScroll threshold="200px" onIonInfinite={ fetchMore }>
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more quotes...">
</IonInfiniteScrollContent>
</IonInfiniteScroll>
</IonRow>
</IonList>
}
{ quotes.length < 1 &&
<IonRow>
<IonCol size="12">
<h3>You haven't saved any bookmarks yet.</h3>
</IonCol>
</IonRow>
}
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Saved;

View File

@@ -0,0 +1,33 @@
import { Store } from "pullstate";
const QuoteStore = new Store({
quotes: [],
saved: []
});
export default QuoteStore;
export const addSavedQuote = id => {
QuoteStore.update(s => { s.saved = [ ...s.saved, id ] });
}
export const removeSavedQuote = id => {
QuoteStore.update(s => { s.saved = s.saved.filter(savedId => parseInt(savedId) !== parseInt(id)) });
}
export const fetchQuotes = async () => {
const response = await fetch("https://type.fit/api/quotes");
const data = await response.json();
await data.filter((quote, index) => {
quote.id = (Date.now() + index);
quote.image = `https://source.unsplash.com/random/1200x400?sig=${ quote.id }`;
});
QuoteStore.update(s => { s.quotes = data });
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
const getState = state => state;
// General getters
export const getQuotes = createSelector(getState, state => state.quotes);
export const getSavedQuotes = createSelector(getState, state => state.saved);
// Specific getters
export const getQuote = id => createSelector(getState, state => state.quotes.filter(q => parseInt(q.id) === parseInt(id))[0]);

View File

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

View File

@@ -0,0 +1,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;
}