Compare commits

...

7 Commits

Author SHA1 Message Date
louiscklaw
bc35e25616 update demo-sliding-profile, 2025-06-08 19:08:01 +08:00
louiscklaw
15f8d2e6aa update demo-shop-app-ui, 2025-06-08 19:08:01 +08:00
louiscklaw
592a099f7b update demo-score-board, 2025-06-08 19:08:01 +08:00
louiscklaw
4c1b30e5c6 update demo-restaurant-finder, 2025-06-08 19:08:00 +08:00
louiscklaw
c765bb49a4 update demo-recipe-app, 2025-06-08 19:08:00 +08:00
louiscklaw
9aeb58379d update demo-react-travel-app, 2025-06-08 19:08:00 +08:00
louiscklaw
6419567005 update demo-react-tabs-menus-custom, 2025-06-08 19:07:48 +08:00
55 changed files with 2462 additions and 178 deletions

View File

@@ -18,7 +18,7 @@ import { SkeletonDashboard } from '../TestComponents/SkeletonDashboard';
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
import { CurrentWeather } from '../TestComponents/CurrentWeather';
function Tab1() {
function Tab1(): React.JSX.Element {
const router = useIonRouter();
const [currentWeather, setCurrentWeather] = useState(false);

View File

@@ -12,7 +12,7 @@ import {
import { useState } from 'react';
import { CurrentWeather } from '../TestComponents/CurrentWeather';
function Tab2() {
function Tab2(): React.JSX.Element {
const [search, setSearch] = useState('');
const [currentWeather, setCurrentWeather] = useState(false);

View File

@@ -2,8 +2,31 @@ import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react'
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
const [property, setProperty] = useState(false);
interface WeatherPropertyProps {
type: 'wind' | 'feelsLike' | 'indexUV' | 'pressure';
currentWeather: {
current: {
wind_mph: number;
feelslike_c: number;
uv: number;
pressure_mb: number;
};
};
}
interface PropertyType {
isIcon: boolean;
icon: string;
alt: string;
label: string;
value: string;
}
export const WeatherProperty = ({
type,
currentWeather,
}: WeatherPropertyProps): React.JSX.Element => {
const [property, setProperty] = useState<PropertyType | false>(false);
const properties = {
wind: {

View File

@@ -1,7 +1,24 @@
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
import { WeatherProperty } from './WeatherProperty';
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
interface CurrentWeatherProps {
currentWeather: {
location: {
region: string;
country: string;
localtime: string;
};
current: {
condition: {
icon: string;
text: string;
};
temp_c: number;
};
};
}
export const CurrentWeather = ({ currentWeather }: CurrentWeatherProps): React.JSX.Element => (
<IonGrid>
<IonCard>
<IonCardContent className="ion-text-center">

View File

@@ -0,0 +1,59 @@
import {
IonGrid,
IonRow,
IonCol,
IonModal,
IonButtons,
IonButton,
IonIcon,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
} from '@ionic/react';
import { chevronBack } from 'ionicons/icons';
interface ModalProps {
showModal: boolean;
close: (value: boolean) => void;
modalOptions: {
text?: string;
name?: string;
icon?: string;
};
}
export const Modal = (props: ModalProps): React.JSX.Element => (
<IonModal isOpen={props.showModal}>
<IonHeader>
<IonToolbar>
<IonTitle>
{props.modalOptions.text ? props.modalOptions.text : props.modalOptions.name}
</IonTitle>
<IonButtons slot="start">
<IonButton onClick={() => props.close(false)}>
<IonIcon size="large" icon={chevronBack} style={{ marginLeft: '-0.7rem' }} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="ion-text-center ion-margin-top">
<IonCol size="12">
<IonIcon style={{ fontSize: '5rem' }} icon={props.modalOptions.icon} />
</IonCol>
</IonRow>
{props.modalOptions.name && (
<IonRow className="ion-text-center">
<IonCol size="12">
<h3>{props.modalOptions.name}</h3>
</IonCol>
</IonRow>
)}
</IonGrid>
</IonContent>
</IonModal>
);

View File

@@ -0,0 +1,17 @@
import { IonRow, IonCol, IonCardSubtitle, IonCardTitle } from '@ionic/react';
interface PageHeaderProps {
pageName: string;
count: number;
}
export const PageHeader = (props: PageHeaderProps): React.JSX.Element => (
<IonRow className="ion-text-center ion-margin-top">
<IonCol size="12">
<IonCardTitle>Tab Menu with Side Menu</IonCardTitle>
<IonCardSubtitle className="ion-margin-top">
{props.pageName} page with {props.count} side menu options
</IonCardSubtitle>
</IonCol>
</IonRow>
);

View File

@@ -19,7 +19,11 @@ function DemoReactTabsMenusCustom() {
<Tab2 />
</Route>
<Redirect exact path="/demo-react-tabs-menus-custom" to="/demo-react-tabs-menus-custom/tab1" />
<Redirect
exact
path="/demo-react-tabs-menus-custom"
to="/demo-react-tabs-menus-custom/tab1"
/>
</IonRouterOutlet>
{/* */}

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { IonHeader, IonContent, IonToolbar, IonTitle, IonButtons, IonMenuButton, IonBackButton, IonIcon, IonSearchbar } from '@ionic/react';
import { chevronBack } from 'ionicons/icons';
const CustomPage = (props) => {
const mainContent = props.children;
const {
name,
sideMenu = false,
sideMenuPosition = "end",
backButton = false,
backButtonIcon = chevronBack,
backButtonText = " ",
backButtonPath,
actionButton = false,
actionButtonPosition,
actionButtonIcon,
actionButtonIconSize,
actionButtonClickEvent,
contentClass,
searchbar = false,
searchbarEvent,
showLargeHeader = true
} = props;
return (
<>
<IonHeader translucent={ true }>
<IonToolbar>
<IonTitle>{ name }</IonTitle>
{ backButton &&
<IonButtons slot="start">
<IonBackButton icon={ backButtonIcon } text={ backButtonText } defaultHref={ backButtonPath } />
</IonButtons>
}
{ (actionButton && actionButtonIcon) &&
<IonButtons slot={ actionButtonPosition }>
<IonIcon style={{ fontSize: actionButtonIconSize }} icon={ actionButtonIcon } onClick={ actionButtonClickEvent }></IonIcon>
</IonButtons>
}
{ sideMenu &&
<IonButtons slot={ sideMenuPosition }>
<IonMenuButton />
</IonButtons>
}
</IonToolbar>
</IonHeader>
<IonContent className={ contentClass } fullscreen>
{ showLargeHeader &&
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle slot="start" size="large">{ name }</IonTitle>
{ searchbar && <IonSearchbar style={{ marginTop: "-0.2rem", width: "50%", float: "right" }} onKeyUp={ e => searchbarEvent(e) } onChange={ e => searchbarEvent(e) } /> }
</IonToolbar>
</IonHeader>
}
{ mainContent }
</IonContent>
</>
);
}
export default CustomPage;

View File

@@ -0,0 +1,34 @@
import { archiveOutline, beerOutline, cogOutline, eyeOutline, golfOutline, logOutOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, pulseOutline, refreshOutline, restaurantOutline, settingsOutline } from "ionicons/icons";
import { buildSideMenuObject } from "./Utils";
export const tab1SideMenu = [
buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"),
buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"),
buildSideMenuObject(true),
buildSideMenuObject(false, "Account Settings", null, settingsOutline, null),
buildSideMenuObject(false, "Settings Sub Page", "Opens settings sub page", cogOutline, "/settings"),
buildSideMenuObject(false, "Privacy", null, eyeOutline, null),
buildSideMenuObject(false, "Logout", null, logOutOutline, null)
];
export const tab2SideMenu = [
buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"),
buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"),
buildSideMenuObject(true),
buildSideMenuObject(false, "Unread", null, mailUnreadOutline, null),
buildSideMenuObject(false, "Archived", null, archiveOutline, null),
buildSideMenuObject(false, "Timestamp style", "Changes the style of the timestamp", refreshOutline, null)
];
export const tab3SideMenu = [
buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"),
buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"),
buildSideMenuObject(true),
buildSideMenuObject(false, "Pubs", null, beerOutline, null),
buildSideMenuObject(false, "Restaurants", null, restaurantOutline, null),
buildSideMenuObject(false, "Golf Courses", null, golfOutline, null),
buildSideMenuObject(false, "Hospitals", null, pulseOutline, null)
];

View File

@@ -0,0 +1,63 @@
import { IonHeader, IonContent, IonToolbar, IonTitle, IonMenuToggle, IonItem, IonIcon, IonMenu, IonLabel, IonList, IonListHeader } from '@ionic/react';
import { useSideMenu } from "../main/SideMenuProvider";
import "../theme/SideMenu.css";
const SideMenu = (props) => {
const { type = "overlay" } = props;
const mainContent = props.children;
const menuOptions = useSideMenu();
return (
<IonMenu contentId={ menuOptions.pageName } side={ menuOptions.side } type={ type }>
<IonHeader>
<IonToolbar>
<IonTitle>Menu</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent forceOverscroll={ false } id="main">
{ mainContent }
<IonListHeader>{ menuOptions.pageName }</IonListHeader>
{ menuOptions !== null &&
<IonList lines="none">
{ menuOptions && menuOptions.options.map((menuOption, i) => {
if (menuOption.url === null) {
return (
<IonMenuToggle key={ i } autoHide={ true }>
<IonItem onClick={ menuOption.clickEvent } lines="none" detail={ false }>
<IonIcon slot="start" icon={ menuOption.icon } />
<IonLabel>{ menuOption.text }</IonLabel>
</IonItem>
</IonMenuToggle>
);
} else {
if (menuOption.url !== null) {
return (
<IonMenuToggle key={ i } autoHide={ true }>
<IonItem detail={ false } routerLink={ menuOption.url } lines="none">
<IonIcon slot="start" icon={ menuOption.icon } />
<IonLabel>{ menuOption.text }</IonLabel>
</IonItem>
</IonMenuToggle>
);
}
}
})}
</IonList>
}
</IonContent>
</IonMenu>
);
}
export default SideMenu;

View File

@@ -0,0 +1,31 @@
import React, { useContext, useState } from "react";
const SideMenuContext = React.createContext();
const SideMenuUpdateContext = React.createContext();
export function useSideMenu() {
return useContext(SideMenuContext);
}
export function useSideMenuUpdate() {
return useContext(SideMenuUpdateContext);
}
export function SideMenuProvider({ children }) {
const [ sideMenuOptions, setSideMenuOptions ] = useState({ options: [], side: "", pageName: "" });
const setSideMenu = (menuOptions) => {
setSideMenuOptions(menuOptions);
}
return (
<SideMenuContext.Provider value={ sideMenuOptions }>
<SideMenuUpdateContext.Provider value={ setSideMenu }>
{children}
</SideMenuUpdateContext.Provider>
</SideMenuContext.Provider>
);
}

View File

@@ -0,0 +1,43 @@
import React from "react";
import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from "@ionic/react";
import { Redirect, Route } from "react-router-dom";
const TabMenu = (props) => {
return (
<IonTabs>
<IonRouterOutlet>
{ props.tabs.map((tab, i) => {
const TabComponent = tab.component;
if (tab.isTab) {
return <Route key={ `tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent { ...props } sideMenu={ tab.sideMenu ? true : false } sideMenuOptions={ tab.sideMenuOptions ? tab.sideMenuOptions : false } /> } exact={ true }/>;
} else {
return <Route key={ `child_tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent {...props} sideMenu={ tab.sideMenu ? true : false } sideMenuOptions={ tab.sideMenuOptions ? tab.sideMenuOptions : false } /> } exact={ false } />;
}
})}
</IonRouterOutlet>
<IonTabBar slot={ props.position }>
{ props.tabs.map((tab, i) => {
if (tab.isTab) {
return (
<IonTabButton key={ `tab_button_${ i + 1 }` } tab={ `tab_${ i + 1 }` } href={ tab.path }>
<IonIcon icon={ tab.icon } />
{ tab.label && <IonLabel>{ tab.label }</IonLabel> }
</IonTabButton>
);
}
})}
</IonTabBar>
</IonTabs>
);
}
export default TabMenu;

View File

@@ -0,0 +1,173 @@
import { beerOutline, golfOutline, pulseOutline, restaurantOutline } from "ionicons/icons";
export const getInboxItems = () => {
return [
{
id: 1,
sender: "Github",
subject: "Host your code here",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "3 mins ago",
unread: true
},
{
id: 2,
sender: "Ionic",
subject: "Amazing cross platform apps on the web",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "2 hrs ago",
unread: false
},
{
id: 3,
sender: "Capacitor",
subject: "This is why capacitor is awesome",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "Yesterday",
unread: false
},
{
id: 4,
sender: "ReactJS",
subject: "Get ready for React 2021",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "Yesterday",
unread: true
},
{
id: 5,
sender: "ContextAPI",
subject: "Global state management!",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "2 days ago",
unread: true
},
{
id: 6,
sender: "Javascript",
subject: "The best language",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "3 days ago",
unread: false
},
{
id: 7,
sender: "Mobile app development",
subject: "Bring your solutions to mobile",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
time: "4 days ago",
unread: false
}
];
}
export const getInboxItemByID = ID => {
const inboxItems = getInboxItems();
const inboxItem = inboxItems.filter(i => parseInt(i.id) === parseInt(ID))[0];
return inboxItem;
}
export const getPlaceItems = () => {
const places = [
{
name: "Rusty Tavern",
rating: 8,
type: "pub",
icon: beerOutline
},
{
name: "Meat Mall",
rating: 5,
type: "restaurant",
icon: restaurantOutline
},
{
name: "Lousy Lager",
rating: 10,
type: "pub",
icon: beerOutline
},
{
name: "Hole in one",
rating: 4,
type: "golf",
icon: golfOutline
},
{
name: "Relief center",
rating: 9,
type: "hospital",
icon: pulseOutline
},
{
name: "Yummy yams",
rating: 2,
type: "restaurant",
icon: restaurantOutline
},
{
name: "Under power of others",
rating: 7,
type: "golf",
icon: golfOutline
},
{
name: "Belfast General",
rating: 10,
type: "hospital",
icon: pulseOutline
},
];
return places;
}
/**
*
* @param {Boolean} spacer Renders a space between above and below item
* @param {String} text The text or "label" to show
* @param {String} description The description to show under the text
* @param {*} icon The icon to show - This should be an imported Ion icon
* @param {String} url The url to navigate to e.g. "/tabs/tab2"
* @param {Function} clickEvent A click event to perform instead of url, leave blank and set in component if it's specific (Should be written like () => function())
* @returns A side menu object
*/
export const buildSideMenuObject = (spacer = false, text = "", description = "", icon = false, url = null, clickEvent = null) => {
const title = text;
if (description !== "" && description !== null) {
text = getInformativeSideMenuItem(text, description);
}
return spacer ? {} : {
title,
text,
icon,
url,
clickEvent
};
}
/**
*
* @param {*} text Text of a side menu object
* @param {*} description Description of a side menu object
* @returns A span and h6 holding the text and description
*/
const getInformativeSideMenuItem = (text, description) => {
return (
<>
<span className="menu-title">{ text }</span>
<br />
<h6 className="sub-menu-title">{ description }</h6>
</>
);
}

View File

@@ -0,0 +1,74 @@
// Main Tabs
import Tab1 from "../../pages/Tab1"
import Tab2 from "../../pages/Tab2";
import Tab3 from "../../pages/Tab3";
// Side Menus
import { tab1SideMenu, tab2SideMenu, tab3SideMenu } from "../PageSideMenus";
// Main tab children
import Settings from "../../pages/Settings";
// Sub pages
import InboxItem from "../../pages/InboxItem";
// Tab icons
import { personOutline, mailOutline, mapOutline } from "ionicons/icons";
// Import custom tab menu
import TabMenu from "../TabMenu";
import SubRoutes from "./SubRoutes";
// Array of objects representing tab pages
// These will be the main tabs across the app
// * PARAMS per tab object *
// isTab = true will make the tab appear
// default = the default tab page to open and be redirected to at "/"
// NOTE: there should only be one default tab (default: true)
// label = the label to show with the tab
// component = the component related to this tab page
// icon = icon to show on the tab bar menu
// path = the path which the tab is accessible
export const tabRoutes = [
{ label: "Profile", component: Tab1, icon: personOutline, path: "/tabs/tab1", default: true, isTab: true, sideMenu: true, sideMenuOptions: tab1SideMenu },
{ label: "Inbox", component: Tab2, icon: mailOutline, path: "/tabs/tab2", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab2SideMenu },
{ label: "Places", component: Tab3, icon: mapOutline, path: "/tabs/tab3", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab3SideMenu }
];
// Array of objects representing children pages of tabs
// * PARAMS per tab object *
// isTab = should always be set to false for these
// component = the component related to this tab page
// path = the path which the tab is accessible
// These pages should be related to tab pages and be held within the same path
// E.g. /tabs/tab1/child
const tabChildrenRoutes = [
{ component: InboxItem, path: "/tabs/tab2/:id", isTab: false },
];
// Array of objects representing sub pages
// * PARAMS per tab object *
// component = the component related to this sub page
// path = the path which the sub page is accessible
// This array should be sub pages which are not directly related to a tab page
// E.g. /child
const subPageRoutes = [
{ component: Settings, path: "/settings" },
];
// Let's combine these together as they need to be controlled within the same IonRouterOutlet
const tabsAndChildrenRoutes = [ ...tabRoutes, ...tabChildrenRoutes ];
// Render sub routes
export const SubPages = () => ( <SubRoutes routes={ subPageRoutes } /> );
// Render tab menu
export const Tabs = () => ( <TabMenu tabs={ tabsAndChildrenRoutes } position="bottom" /> );

View File

@@ -0,0 +1,28 @@
import { IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { Redirect, Route } from "react-router-dom";
import SideMenu from "../SideMenu";
import { SubPages, Tabs, tabRoutes } from "./AllRoutes";
const NavRoutes = () => {
return (
<IonReactRouter>
<IonSplitPane contentId="main">
<SideMenu />
<IonRouterOutlet id="main">
<Route path="/tabs" render={ () => <Tabs />} />
<SubPages />
<Route path="/" component={ tabRoutes.filter(t => t.default)[0].component } exact={ true } />
<Redirect exact from="/" to={ tabRoutes.filter(t => t.default)[0].path.toString() }/>
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
);
}
export default NavRoutes;

View File

@@ -0,0 +1,17 @@
import { Route } from "react-router-dom";
const SubRoutes = (props) => {
return (
<>
{ props.routes.map((route, i) => {
const RouteComponent = route.component;
return <Route key={ i } path={ route.path } render={ (props) => <RouteComponent { ...props } sideMenu={ route.sideMenu ? true : false } sideMenuOptions={ route.sideMenuOptions ? route.sideMenuOptions : false } /> } exact={ false } />;
})}
</>
);
}
export default SubRoutes;

View File

@@ -0,0 +1,40 @@
#view-inbox-item ion-item {
--inner-padding-end: 0;
--background: transparent;
}
#view-inbox-item ion-label {
margin-top: 12px;
margin-bottom: 12px;
}
#view-inbox-item ion-item h2 {
font-weight: 600;
}
#view-inbox-item ion-item .date {
float: right;
align-items: center;
display: flex;
}
#view-inbox-item ion-item ion-icon {
font-size: 42px;
margin-right: 8px;
}
#view-inbox-item ion-item ion-note {
font-size: 15px;
margin-right: 12px;
font-weight: normal;
}
#view-inbox-item h1 {
margin: 0;
font-weight: bold;
font-size: 22px;
}
#view-inbox-item p {
line-height: 22px;
}

View File

@@ -0,0 +1,60 @@
import { useEffect, useState } from "react";
import { personCircle } from 'ionicons/icons';
import './Tab2.css';
import CustomPage from "../main/CustomPage";
import { IonIcon, IonItem, IonLabel, IonNote, IonPage, useIonViewWillEnter } from '@ionic/react';
import { useParams } from "react-router";
import { getInboxItemByID } from "../main/Utils";
import "./InboxItem.css";
const InboxItem = props => {
const pageName = "Inbox";
const params = useParams();
const [ inboxItem, setInboxItem ] = useState({});
useIonViewWillEnter(() => {
const inboxItemID = params.id;
const tempInboxItem = getInboxItemByID(inboxItemID);
setInboxItem(tempInboxItem);
});
return (
<IonPage id="view-inbox-item">
<CustomPage showLargeHeader={ false } name={ pageName } sideMenu={ false } backButton={ true } backButtonText="Inbox">
{ inboxItem ? (
<>
<IonItem>
<IonIcon icon={ personCircle } color="primary"></IonIcon>
<IonLabel className="ion-text-wrap">
<h2>
{ inboxItem.sender }
<span className="date">
<IonNote>{ inboxItem.time }</IonNote>
</span>
</h2>
<h3>
To: <IonNote>Me</IonNote>
</h3>
</IonLabel>
</IonItem>
<div className="ion-padding">
<h1>{ inboxItem.subject }</h1>
<p>{ inboxItem.message }</p>
</div>
</>
) : (
<div>Message not found</div>
)}
</CustomPage>
</IonPage>
);
}
export default InboxItem;

View File

@@ -0,0 +1,27 @@
import { IonCol, IonGrid, IonPage, IonRow } from '@ionic/react';
import { addOutline } from 'ionicons/icons';
import './Tab1.css';
import CustomPage from "../main/CustomPage";
const Settings = props => {
const pageName = "Settings";
return (
<IonPage id={ pageName }>
<CustomPage name={ pageName } sideMenu={ false } sideMenuPosition="start" backButton={ true } backButtonText="Profile" actionButton={ true } actionButtonIcon={ addOutline } actionButtonPosition="end" actionButtonIconSize="1.7rem">
<IonGrid>
<IonRow className="ion-text-center">
<IonCol size="12">
<h3>Sub page</h3>
</IonCol>
</IonRow>
</IonGrid>
</CustomPage>
</IonPage>
);
}
export default Settings;

View File

@@ -0,0 +1,5 @@
.role {
float: right;
align-items: center;
display: flex;
}

View File

@@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import { IonAvatar, IonBadge, IonButton, IonCol, IonGrid, IonIcon, IonImg, IonItem, IonLabel, IonNote, IonPage, IonRow, IonText } from '@ionic/react';
import { cogOutline, eyeOutline, logOutOutline, mailOutline, mapOutline, settingsOutline } from 'ionicons/icons';
import './Tab1.css';
import CustomPage from "../main/CustomPage";
import { PageHeader } from "../components/PageHeader";
import { Modal } from "../components/Modal";
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
import { Link } from "react-router-dom";
import { tab1SideMenu } from "../main/PageSideMenus";
const Tab1 = props => {
const pageName = "Profile";
const { sideMenuOptions } = props;
const setSideMenu = useSideMenuUpdate();
const [ showModal, setShowModal ] = useState(false);
const [ modalOptions, setModalOptions ] = useState(false);
const handleModal = async (index) => {
await setModalOptions(tab1SideMenu[index]);
setShowModal(true);
}
// Access other side menu options here
const sideMenu = useSideMenu();
useEffect(() => {
if (props.location.pathname === "/tabs/tab1") {
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
}
}, [ props.location ]);
return (
<IonPage id={ pageName }>
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="start">
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
<IonItem lines="none">
<IonAvatar>
<IonImg src="/assets/alan.jpg" />
</IonAvatar>
<IonLabel className="ion-text-wrap ion-padding">
<h1>Author</h1>
<h2>
Alan Montgomery
<span className="role">
<IonBadge color="primary">Mobile Team Lead</IonBadge>
</span>
</h2>
<p>
Hey there, I'm Alan! Hopefully you can take something away from this little sample app. Or even if it's to have a poke around and see how I personally like to do things, that's OK too 👏🏻. Check out each page, side menu and have a look at how things work.
</p>
</IonLabel>
</IonItem>
<IonGrid>
<IonRow className="ion-text-center">
<IonCol size="12">
<IonText color="primary">
<p>Contact me on twitter if you need anything else :)</p>
<a href="https://twitter.com/intent/tweet?screen_name=93alan&ref_src=twsrc%5Etfw" className="twitter-mention-button" data-size="large" data-related="93alan,93alan" data-dnt="true" data-show-count="false">Tweet to @93alan</a>
</IonText>
</IonCol>
</IonRow>
<IonRow className="ion-text-center">
<IonCol size="12">
<IonText>
<h4>Check out Mobile DevCast</h4>
<p>A podcast dedicated to mobile app development and web native technology like ionic & capacitor!</p>
<IonText color="warning">
<a style={{ color: "yellow" }} href="https://mobiledevcast.com" target="_blank" rel="noreferrer">https://mobiledevcast.com</a>
</IonText>
</IonText>
</IonCol>
</IonRow>
</IonGrid>
{ (showModal && modalOptions) &&
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
}
</CustomPage>
</IonPage>
);
}
export default Tab1;

View File

@@ -0,0 +1,85 @@
import { useEffect, useState } from "react";
import { archiveOutline, checkmarkOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, refreshOutline, settingsSharp } from 'ionicons/icons';
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
import './Tab2.css';
import CustomPage from "../main/CustomPage";
import { PageHeader } from "../components/PageHeader";
import { Modal } from "../components/Modal";
import { IonBadge, IonChip, IonGrid, IonItem, IonLabel, IonList, IonNote, IonPage } from '@ionic/react';
import { getInboxItems } from "../main/Utils";
const Tab2 = props => {
const pageName = "Inbox";
var { sideMenuOptions } = props;
const setSideMenu = useSideMenuUpdate();
const [ Badge, setBadge ] = useState(true);
const [ showModal, setShowModal ] = useState(false);
const [ modalOptions, setModalOptions ] = useState(false);
const inboxItems = getInboxItems();
const handleModal = async (index) => {
await setModalOptions(sideMenuOptions[index]);
setShowModal(true);
}
// Access other side menu options here
const sideMenu = useSideMenu();
useEffect(() => {
if (props.location.pathname === "/tabs/tab2") {
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
sideMenuOptions = sideMenuOptions.filter(m => m.title === "Timestamp style")[0].clickEvent = () => setBadge(Badge => !Badge);
}
}, [ props.location ]);
return (
<IonPage id={ pageName }>
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="end">
<IonGrid>
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
<IonList>
{ inboxItems.map((item, index) => {
return (
<IonItem routerLink={ `/tabs/tab2/${ item.id }` } key={ `item_${ index }`} detail={ true } lines="full" detailIcon={ item.unread ? mailUnreadOutline : checkmarkOutline }>
<IonLabel>
<h2>{ item.sender }</h2>
<h4>{ item.subject }</h4>
<p>{ item.message }</p>
</IonLabel>
{ Badge &&
<IonBadge slot="end" style={{ fontSize: "0.7rem" }}>
{ item.time }
</IonBadge>
}
{ !Badge &&
<IonNote slot="end" style={{ fontSize: "0.9rem" }}>
{ item.time }
</IonNote>
}
</IonItem>
);
})}
</IonList>
</IonGrid>
{ (showModal && modalOptions) &&
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
}
</CustomPage>
</IonPage>
);
}
export default Tab2;

View File

@@ -0,0 +1,97 @@
import { useEffect, useState } from "react";
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
import './Tab3.css';
import CustomPage from "../main/CustomPage";
import { PageHeader } from "../components/PageHeader";
import { Modal } from "../components/Modal";
import { IonPage, IonGrid, IonList, IonItem, IonLabel, IonAvatar, IonIcon, IonBadge } from '@ionic/react';
import { getPlaceItems } from "../main/Utils";
const Tab3 = props => {
const pageName = "Places";
const { sideMenuOptions } = props;
const setSideMenu = useSideMenuUpdate();
const initialPlaceItems = getPlaceItems();
const [ showModal, setShowModal ] = useState(false);
const [ modalOptions, setModalOptions ] = useState(false);
const [ placeItems, setPlaceItems ] = useState(initialPlaceItems);
const handleClick = async (item) => {
await setModalOptions(item);
setShowModal(true);
}
const search = (e) => {
const searchVal = e.target.value;
setPlaceItems(initialPlaceItems);
if (searchVal !== "") {
const newItems = initialPlaceItems.filter((item, index) => {
if (item.name.toLowerCase().includes(searchVal.toLowerCase())) {
item.originalIndex = index;
return true;
}
});
setPlaceItems(newItems);
} else {
setPlaceItems(initialPlaceItems);
}
}
// Access other side menu options here
const sideMenu = useSideMenu();
useEffect(() => {
if (props.location.pathname === "/tabs/tab3") {
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
}
}, [ props.location ]);
return (
<IonPage id={ pageName }>
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="start" searchbar={ true } searchbarEvent={ search }>
<IonGrid>
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
<IonList>
{ placeItems.map((item, index) => {
return (
<IonItem onClick={ () => handleClick(item) } key={ `placeItem_${ index }`} detail={ true } lines="full">
<IonAvatar>
<IonIcon size="large" icon={ item.icon } />
</IonAvatar>
<IonLabel style={{ padding: "1rem" }}>
<h2>{ item.name }</h2>
</IonLabel>
<IonBadge color="dark" slot="end">
{ item.rating } / 10
</IonBadge>
</IonItem>
);
})}
</IonList>
</IonGrid>
{ (showModal && modalOptions) &&
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
}
</CustomPage>
</IonPage>
);
}
export default Tab3;

View File

@@ -0,0 +1,24 @@
ion-menu ion-content {
--padding-top: 1.5rem;
--padding-bottom: 1.5rem;
}
ion-menu ion-item {
--padding-start: 1rem;
--min-height: 3.5rem;
font-weight: 500;
}
ion-menu ion-item ion-icon {
font-size: 1.6rem;
color: #757575;
}
ion-menu ion-item .sub-menu-title {
color: var(--ion-color-primary) !important;
font-size: 0.75rem !important;
}

View File

@@ -0,0 +1,236 @@
/* 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: #428cff;
--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: #ff4961;
--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: #000000;
--ion-toolbar-background: #000000;
--ion-toolbar-border-color: var(--ion-color-step-150);
}
/*
* 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;
}
}

View File

@@ -1,99 +1,102 @@
import { IonCard, IonCardHeader, IonCardTitle, IonNote, useIonToast, CreateAnimation, IonIcon } from "@ionic/react";
import { useState } from "react";
import { useRef } from "react";
import { Iconly } from "react-iconly";
import { heart, trashBin } from "ionicons/icons";
import { addFavourite } from "../store/PlacesStore";
import {
IonCard,
IonCardHeader,
IonCardTitle,
IonNote,
useIonToast,
CreateAnimation,
IonIcon,
} from '@ionic/react';
import { useState } from 'react';
import { useRef } from 'react';
import { Iconly } from 'react-iconly';
import { heart, trashBin } from 'ionicons/icons';
import { addFavourite } from '../store/PlacesStore';
import styles from "../styles/Home.module.scss";
import styles from '../styles/Home.module.scss';
const PlaceCard = ({ place = false, fromFavourites = false }) => {
const animationRef = useRef(null);
const cardRef = useRef(null);
const [presentToast] = useIonToast();
const [hideAnimatedIcon, setHideAnimatedIcon] = useState(true);
const animationRef = useRef();
const cardRef = useRef();
const [ presentToast ] = useIonToast();
const [ hideAnimatedIcon, setHideAnimatedIcon ] = useState(true);
const floatStyle = {
display: hideAnimatedIcon ? 'none' : '',
position: 'absolute',
zIndex: '10',
};
const floatStyle = {
const floatGrowAnimation = {
property: 'transform',
fromValue: 'translateY(0) scale(1)',
toValue: 'translateY(-20px) scale(2)',
};
display: hideAnimatedIcon ? "none" : "",
position: "absolute",
zIndex: "10"
};
const mainAnimation = {
duration: 600,
iterations: '1',
fromTo: [floatGrowAnimation],
easing: 'cubic-bezier(0.25, 0.7, 0.25, 0.7)',
};
const floatGrowAnimation = {
const handleAddFavourite = async (e, place) => {
e.stopPropagation();
e.preventDefault();
property: "transform",
fromValue: "translateY(0) scale(1)",
toValue: "translateY(-20px) scale(2)"
};
if (fromFavourites) {
// Add a fadeOut animation before removing
cardRef.current.classList.add('animate__fadeOut');
const mainAnimation = {
setTimeout(() => {
addFavourite(place, fromFavourites);
}, 500);
} else {
addFavourite(place, fromFavourites);
}
duration: 600,
iterations: "1",
fromTo: [ floatGrowAnimation ],
easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
};
presentToast({
header: `Favourite ${fromFavourites ? 'removed' : 'added'}!`,
buttons: [
{
text: '♡',
},
],
message: `${place.name} has been ${fromFavourites ? 'removed from' : 'added to'} your favourites.`,
duration: 1500,
color: 'success',
});
const handleAddFavourite = async (e, place) => {
setHideAnimatedIcon(false);
await animationRef.current.animation.play();
setHideAnimatedIcon(true);
};
e.stopPropagation();
e.preventDefault();
if (fromFavourites) {
return (
<IonCard
ref={cardRef}
className={`${styles.slide} animate__animated animate__fadeIn animate__faster`}
routerLink={`/view-place/${place.id}`}
>
<div className={styles.imageHeader}>
<img src={place ? place.image : '/assets/nonefound.png'} />
{place && (
<div className="favouriteButton" onClick={(e) => handleAddFavourite(e, place)}>
<Iconly set="bold" name={fromFavourites ? 'Delete' : 'Heart'} color="red" />
// Add a fadeOut animation before removing
cardRef.current.classList.add("animate__fadeOut");
<CreateAnimation ref={animationRef} {...mainAnimation}>
<IonIcon icon={fromFavourites ? trashBin : heart} style={floatStyle} color="danger" />
</CreateAnimation>
</div>
)}
</div>
setTimeout(() => {
addFavourite(place, fromFavourites);
}, 500);
} else {
<IonCardHeader>
<IonCardTitle>{place ? place.name : 'Sorry'}</IonCardTitle>
<IonNote>{place ? place.destination : 'No results found'}</IonNote>
</IonCardHeader>
</IonCard>
);
};
addFavourite(place, fromFavourites);
}
presentToast({
header: `Favourite ${ fromFavourites ? "removed" : "added" }!`,
buttons: [
{
text: "♡",
}
],
message: `${ place.name } has been ${ fromFavourites ? "removed from" : "added to" } your favourites.`,
duration: 1500,
color: "success"
});
setHideAnimatedIcon(false);
await animationRef.current.animation.play();
setHideAnimatedIcon(true);
}
return (
<IonCard ref={ cardRef } className={ `${ styles.slide } animate__animated animate__fadeIn animate__faster` } routerLink={ `/view-place/${ place.id }` }>
<div className={ styles.imageHeader }>
<img src={ place ? place.image : "/assets/nonefound.png" } />
{ place &&
<div className="favouriteButton" onClick={ e => handleAddFavourite(e, place) }>
<Iconly set="bold" name={ fromFavourites ? "Delete" : "Heart" } color="red" />
<CreateAnimation ref={ animationRef } { ...mainAnimation }>
<IonIcon icon={ fromFavourites ? trashBin : heart } style={ floatStyle } color="danger" />
</CreateAnimation>
</div>
}
</div>
<IonCardHeader>
<IonCardTitle>{ place ? place.name : "Sorry" }</IonCardTitle>
<IonNote>{ place ? place.destination : "No results found" }</IonNote>
</IonCardHeader>
</IonCard>
);
}
export default PlaceCard;
export default PlaceCard;

View File

@@ -26,7 +26,7 @@ import { BookmarkStore } from '../store';
const Categories = () => {
const router = useIonRouter();
const pageRef = useRef();
const pageRef = useRef(null);
const [recipeCategories, setRecipeCategories] = useState([]);
const bookmarks = useStoreState(BookmarkStore, getBookmarks);

View File

@@ -39,7 +39,7 @@ import { useStoreState } from 'pullstate';
import { getBookmarks } from '../store/Selectors';
const Recipe = () => {
const pageRef = useRef();
const pageRef = useRef(null);
const { state } = useLocation();
const [recipe, setRecipe] = useState([]);
const [fromSearch, setFromSearch] = useState(false);

View File

@@ -25,7 +25,7 @@ import { performSearch } from '../utils';
import { RecipeListItem } from '../components/RecipeListItem';
const Search = () => {
const searchRef = useRef();
const searchRef = useRef(null);
const [searchResults, setSearchResults] = useState([]);
const [showLoader, hideLoader] = useIonLoading();

View File

@@ -0,0 +1,16 @@
import { IonItem, IonLabel } from "@ionic/react";
import styles from "./Ingredient.module.scss";
export const Ingredient = ({ ingredient }) => {
return (
<IonItem lines="full" className={ styles.ingredientItem }>
<img alt="ingredient" src={ ingredient.image } className={ styles.ingredientImage } />
<IonLabel className="ion-text-wrap ion-margin-start">
<h3>{ ingredient.text }</h3>
<p>{ ingredient.weight.toFixed(2) }g</p>
</IonLabel>
</IonItem>
);
}

View File

@@ -0,0 +1,13 @@
.ingredientImage {
height: 3rem;
width: 3rem;
border-radius: 10px;
border: 1px solid rgb(172, 172, 172);
}
.ingredientItem {
--padding-top: 1rem;
--padding-bottom: 1rem;
}

View File

@@ -0,0 +1,30 @@
import { IonButton, IonButtons, IonContent, IonGrid, IonHeader, IonList, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
import { Ingredient } from "./Ingredient";
const IngredientsModal = ({ dismiss, ingredients}) => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>View Ingredients</IonTitle>
<IonButtons slot="start">
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
{ ingredients.map((ingredient, index) => {
return <Ingredient key={ `ingredient_${ index }` } ingredient={ ingredient } />;
})}
</IonList>
</IonContent>
</IonPage>
);
}
export default IngredientsModal;

View File

@@ -0,0 +1,48 @@
import { IonButton, IonButtons, IonCardSubtitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
import NutritionalFact from "./NutritionalFact";
const NutritionModal = ({ dismiss, recipe }) => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>View Nutrition</IonTitle>
<IonButtons slot="start">
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
{ (recipe && recipe.digest) &&
<IonGrid>
<IonRow>
<IonCol size="12">
<IonCardSubtitle className="ion-text-center" color="main">
Based on a serving size of { recipe.totalWeight.toFixed(0) }g
</IonCardSubtitle>
<NutritionalFact type="calories" amount={ recipe.calories.toFixed(0) } />
<NutritionalFact type="fat" amount={ recipe.digest[0].total.toFixed(0) } />
<NutritionalFact type="trans_fat" amount={ recipe.digest[0].sub[1].total.toFixed(0) } inset={ true } />
<NutritionalFact type="saturated_fat" amount={ recipe.digest[0].sub[0].total.toFixed(0) } inset={ true } />
<NutritionalFact type="polyunsaturated_fat" amount={ recipe.digest[0].sub[3].total.toFixed(0) } inset={ true } />
<NutritionalFact type="monounsaturated_fat" amount={ recipe.digest[0].sub[2].total.toFixed(0) } inset={ true } />
<NutritionalFact type="carbs" amount={ recipe.digest[1].total.toFixed(0) } />
<NutritionalFact type="sugar" amount={ recipe.digest[1].sub[2].total.toFixed(0) } inset={ true } />
<NutritionalFact type="fibre" amount={ recipe.digest[1].sub[1].total.toFixed(0) } inset={ true } />
<NutritionalFact type="sugars_added" amount={ recipe.digest[1].sub[3].total.toFixed(0) } inset={ true } />
<NutritionalFact type="protein" amount={ recipe.digest[2].total.toFixed(0) } />
</IonCol>
</IonRow>
</IonGrid>
}
</IonContent>
</IonPage>
);
}
export default NutritionModal;

View File

@@ -0,0 +1,25 @@
import { IonCardTitle, IonCol, IonRow } from "@ionic/react";
const NutritionalFact = ({ type, amount, inset }) => {
const label = type.replace("_", " ").replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
return (
<IonRow style={{ borderBottom: "1px solid #242424", padding: "0.5rem" }}>
<IonCol size="9">
<IonCardTitle style={{ fontSize: "0.9rem", marginLeft: inset ? "1.5rem" : "" }}>
{ label }
</IonCardTitle>
</IonCol>
<IonCol size="3">
<IonCardTitle style={{ fontSize: "0.9rem" }}>
{ amount }{ type !== "calories" && "g" }
</IonCardTitle>
</IonCol>
</IonRow>
);
}
export default NutritionalFact;

View File

@@ -0,0 +1,18 @@
import { IonItem, IonLabel } from "@ionic/react";
import { Link } from "react-router-dom";
import styles from "./RecipeListItem.module.scss";
export const RecipeListItem = ({ recipe, fromSearch = false, fromBookmarks = false }) => (
<Link to={{ pathname: `/recipe/${ recipe.label.replace(" ", "").toLowerCase() }`, state: { recipe, fromSearch, fromBookmarks }}}>
<IonItem detail={ true } lines="full" className={ styles.categoryItem }>
<img src={ recipe.image } alt="cover" className={ styles.categoryImage } />
<IonLabel className={ styles.categoryDetails }>
<h2>{ recipe.label }</h2>
<p>{ recipe.dishType && recipe.dishType[0] }</p>
</IonLabel>
</IonItem>
</Link>
);

View File

@@ -0,0 +1,17 @@
.categoryDetails {
margin-left: 1rem;
}
.categoryImage {
width: 5rem;
height: 5rem;
border-radius: 10px;
}
.categoryItem {
--padding-top: 1.5rem;
--padding-bottom: 1.5rem;
}

View File

@@ -0,0 +1,65 @@
import { IonBackButton, IonButtons, IonCol, IonContent, IonHeader, IonImg, IonList, IonNote, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react';
import { RecipeListItem } from '../components/RecipeListItem';
import { useStoreState } from "pullstate";
import { BookmarkStore } from '../store';
import { getBookmarks } from '../store/Selectors';
const Bookmarks = () => {
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton text="Categories" />
</IonButtons>
<IonTitle>Bookmarks</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Bookmarks ({ bookmarks.length })</IonTitle>
</IonToolbar>
</IonHeader>
<IonList>
{ bookmarks.map((bookmark, index) => {
return (
<RecipeListItem recipe={ bookmark } key={ `recipe_${ index }` } fromBookmarks={ true } />
);
})}
</IonList>
{ bookmarks.length < 1 &&
<>
<IonRow className="ion-justify-content-center ion-text-center ion-margin-top ion-padding-top">
<IonCol size="8">
<IonText>
You don't have any bookmarks yet
</IonText>
<IonNote>
<p>When viewing a recipe, press the bookmark icon to add it</p>
</IonNote>
</IonCol>
</IonRow>
<IonRow className="ion-justify-content-center">
<IonCol size="8">
<IonImg src="/assets/bookmark.png" />
</IonCol>
</IonRow>
</>
}
</IonContent>
</IonPage>
);
};
export default Bookmarks;

View File

@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import { IonButton, IonButtons, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonSearchbar, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
import styles from "./Categories.module.scss";
import { recipes } from "../recipes";
import { useRef } from "react";
import { Link } from "react-router-dom";
import { bookmarkOutline } from "ionicons/icons";
import { getBookmarks } from "../store/Selectors";
import { useStoreState } from "pullstate";
import { BookmarkStore } from "../store";
const Categories = () => {
const router = useIonRouter();
const pageRef = useRef();
const [ recipeCategories, setRecipeCategories ] = useState([]);
const bookmarks = useStoreState(BookmarkStore, getBookmarks)
useEffect(() => {
const getAllRecipes = async () => {
const tempRecipeCategories = [
{
name: "Chicken",
data: recipes.chicken.hits[0]
},
{
name: "Beef",
data: recipes.beef.hits[0]
},
{
name: "Fish",
data: recipes.fish.hits[0]
},
{
name: "Fruit",
data: recipes.fruit.hits[0]
},
{
name: "Salad",
data: recipes.salad.hits[0]
},
{
name: "Vegan",
data: recipes.vegan.hits[0]
}
];
setRecipeCategories(tempRecipeCategories);
}
getAllRecipes();
}, []);
return (
<IonPage ref={ pageRef }>
<IonHeader className="ion-no-border">
<IonToolbar className="ion-no-border">
<IonTitle>Recipe Categories</IonTitle>
<IonButtons slot="end">
<IonButton routerLink="/bookmarks">
<IonIcon icon={ bookmarkOutline } />&nbsp;
{ bookmarks.length }
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div className={ styles.searchArea }>
<IonSearchbar className="ion-justify-content-center" placeholder="Try 'Chicken Piccata'" onClick={ () => router.push("/search") } />
</div>
<IonGrid>
<IonRow className={ styles.row }>
{ recipeCategories.map((category, index) => {
const { name, data } = category;
const { recipe } = data;
return (
<IonCol className={ styles.col } size="6" key={ `recipe_${ index }` }>
<Link to={ `/category/${ name }`}>
<img src={ recipe.image } alt="cover" />
<div className={ styles.categoryName }>
<IonCardTitle>{ name }</IonCardTitle>
</div>
</Link>
</IonCol>
);
})}
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Categories;

View File

@@ -0,0 +1,65 @@
.categoryName {
position: absolute;
left: 0;
top: 0;
margin: 0 auto;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 10px;
height: 100%;
width: 100%;
ion-card-title {
color: white;
margin-left: 0.5rem;
margin-top: 0.5rem;
padding: 0.5rem;
width: fit-content;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 10px;
}
}
.row,
.col,
.card {
padding: 0;
background-color: white;
border-radius: 10px;
}
.col {
border: 5px solid white;
img {
border-radius: 10px;
height: 100%;
width: 100%;
}
}
.searchArea {
background-color: var(--ion-toolbar-background);
padding-bottom: 0.5rem;
ion-searchbar {
color: white;
--background: rgb(49, 49, 49);
--icon-color: rgb(27, 173, 100);
// --border-radius: 0;
}
}
.searchButton {
height: 2.2rem;
margin-top: 0.9rem;
}

View File

@@ -0,0 +1,50 @@
import { IonBackButton, IonButtons, IonContent, IonHeader, IonList, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import { useEffect } from 'react';
import { useState } from 'react';
import { useParams } from 'react-router';
import { RecipeListItem } from '../components/RecipeListItem';
import { recipes } from '../recipes';
const Category = () => {
const { name } = useParams();
const [ categoryRecipes, setCategoryRecipes ] = useState([]);
useEffect(() => {
setCategoryRecipes(recipes[name.toLowerCase()].hits);
}, [ name ]);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton text="Categories" />
</IonButtons>
<IonTitle>{ name } Recipes</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">{ name } Recipes</IonTitle>
</IonToolbar>
</IonHeader>
<IonList>
{ categoryRecipes.map((categoryRecipe, index) => {
const { recipe } = categoryRecipe;
return (
<RecipeListItem recipe={ recipe } key={ `recipe_${ index }` } />
);
})}
</IonList>
</IonContent>
</IonPage>
);
};
export default Category;

View File

@@ -0,0 +1,165 @@
import { IonBackButton, IonButton, IonButtons, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonList, IonListHeader, IonPage, IonRow, IonTitle, IonToolbar, useIonModal, useIonToast } from '@ionic/react';
import { bookmark, bookmarkOutline, informationCircleOutline, layersOutline, peopleOutline, timeOutline } from 'ionicons/icons';
import { useEffect, useRef } from 'react';
import { useState } from 'react';
import { useLocation } from 'react-router';
import { Ingredient } from '../components/Ingredient';
import IngredientsModal from '../components/IngredientsModal';
import NutritionModal from '../components/NutritionModal';
import BookmarkStore, { addToBookmarks } from '../store/BookmarkStore';
import styles from "./Recipe.module.scss";
import { useStoreState } from "pullstate";
import { getBookmarks } from '../store/Selectors';
const Recipe = () => {
const pageRef = useRef();
const { state } = useLocation();
const [ recipe, setRecipe ] = useState([]);
const [ fromSearch, setFromSearch ] = useState(false);
const [ fromBookmarks, setFromBookmarks ] = useState(false);
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
const [ showToast ] = useIonToast();
const handleDismissIngredientsModal = () => {
hideIngredientsModal();
}
const handleDismissNutritionModal = () => {
hideNutritionModal();
}
const [ showIngredientsModal, hideIngredientsModal ] = useIonModal(IngredientsModal, {
dismiss: handleDismissIngredientsModal,
ingredients: recipe.ingredients
});
const [ showNutritionModal, hideNutritionModal ] = useIonModal(NutritionModal, {
dismiss: handleDismissNutritionModal,
recipe
});
useEffect(() => {
if (state && state.recipe) {
setRecipe(state.recipe);
}
if (state && state.fromSearch) {
setFromSearch(state.fromSearch);
}
if (state && state.fromBookmarks) {
setFromBookmarks(state.fromBookmarks);
}
}, [ state ]);
const addBookmark = async () => {
const added = addToBookmarks(recipe);
showToast({
message: added ? "This recipe has been bookmarked!" : "This recipe has been removed from your bookmarks.",
duration: 2000,
color: "main"
});
}
return (
<IonPage ref={ pageRef }>
<IonHeader>
<IonToolbar>
<IonTitle>View Recipe</IonTitle>
<IonButtons slot="start">
<IonBackButton text={ fromSearch ? "Search" : fromBookmarks ? "Bookmarks" : "Recipes" } color="main" />
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={ addBookmark }>
<IonIcon icon={ bookmarks.includes(recipe) ? bookmark : bookmarkOutline } />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div className={ styles.headerImage }>
<img src={ recipe.image } alt="main cover" />
<div className={ `${ styles.headerInfo } animate__animated animate__slideInLeft` }>
<h1>{ recipe.label }</h1>
<p>{ recipe.dishType && recipe.dishType[0] }</p>
</div>
</div>
<IonGrid>
<IonRow className="ion-text-center">
<IonCol size="4">
<IonCardTitle>
<IonIcon icon={ peopleOutline } />
</IonCardTitle>
<IonCardSubtitle>serves { recipe.yield && recipe.yield }</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>
<IonIcon icon={ timeOutline } />
</IonCardTitle>
<IonCardSubtitle>{ recipe.totalTime !== 0 ? `${ recipe.totalWeight && recipe.totalWeight.toFixed(0) } mins` : "N/A" }</IonCardSubtitle>
</IonCol>
<IonCol size="4">
<IonCardTitle>
<IonIcon icon={ layersOutline } />
</IonCardTitle>
<IonCardSubtitle>{ recipe.totalWeight && recipe.totalWeight.toFixed(0) }g</IonCardSubtitle>
</IonCol>
</IonRow>
<IonRow className="ion-text-center">
<IonCol size="6">
{/* <IonButton color="main" onClick={ () => showIngredientsModal({
presentingElement: pageRef.current,
cssClass: "customModal"
})}>
<IonIcon icon={ informationCircleOutline } />&nbsp;
View Ingredients
</IonButton> */}
</IonCol>
<IonCol size="12">
<IonButton expand="block" color="main" onClick={ () => showNutritionModal({
presentingElement: pageRef.current,
cssClass: "customModal"
})}>
<IonIcon icon={ informationCircleOutline } />&nbsp;
View Nutrition
</IonButton>
</IonCol>
</IonRow>
{ recipe.ingredients &&
<IonList>
<IonListHeader>Ingredients ({ recipe.ingredients.length })</IonListHeader>
{ recipe.ingredients.map((ingredient, index) => {
return <Ingredient key={ `ingredient_${ index }` } ingredient={ ingredient } />;
})}
</IonList>
}
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Recipe;

View File

@@ -0,0 +1,37 @@
.headerImage {
img {
width: 100%;
margin-top: -5rem;
border-bottom: 5px solid var(--ion-color-main);
}
.headerInfo {
position: absolute;
top: 10rem;
z-index: 10;
background-color: rgba($color: #000000, $alpha: 0.8);
color: white;
padding: 1rem;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
h1 {
font-size: 1.1rem;
padding: 0;
margin: 0;
}
p {
font-size: 0.9rem;
padding: 0;
margin: 0;
color: var(--ion-color-main);
}
}
}

View File

@@ -0,0 +1,106 @@
import { IonBackButton, IonButton, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonImg, IonList, IonNote, IonPage, IonRow, IonSearchbar, IonText, IonTitle, IonToolbar, useIonLoading, useIonViewDidEnter } from "@ionic/react"
import { useState } from "react";
import { useRef } from "react";
import styles from "./Categories.module.scss";
import { performSearch } from "../utils";
import { RecipeListItem } from "../components/RecipeListItem";
const Search = () => {
const searchRef = useRef();
const [ searchResults, setSearchResults ] = useState([]);
const [ showLoader, hideLoader ] = useIonLoading();
useIonViewDidEnter(() => {
searchRef.current.setFocus();
});
const search = async () => {
showLoader({
cssClass: "customLoader",
message: "Searching...",
duration: 9999999,
spinner: "dots"
});
const searchTerm = searchRef.current.value;
const data = await performSearch(searchTerm);
setSearchResults(data.hits);
setTimeout(() => {
hideLoader();
}, 300);
}
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Search Recipes</IonTitle>
<IonButtons slot="start">
<IonBackButton color="main" text="Categories" />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<div className={ styles.searchArea }>
<IonGrid>
<IonRow>
<IonCol size="9">
<IonSearchbar ref={ searchRef } placeholder="Try 'Chicken Piccata'" />
</IonCol>
<IonCol size="3">
<IonButton className={ styles.searchButton } expand="block" color="main" onClick={ search }>Search</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</div>
{ searchResults.length > 0 &&
<IonList>
{ searchResults.map((result, index) => {
const { recipe } = result;
return (
<RecipeListItem recipe={ recipe } key={ `result_${ index }` } fromSearch={ true } />
);
})}
</IonList>
}
{ searchResults.length < 1 &&
<>
<IonRow className="ion-justify-content-center ion-text-center ion-margin-top ion-padding-top">
<IonCol size="8">
<IonText>
Search for a recipe then select from the list to view it
</IonText>
<IonNote>
<p>For development purposes, only 20 results will be returned</p>
</IonNote>
</IonCol>
</IonRow>
<IonRow className="ion-justify-content-center">
<IonCol size="8">
<IonImg src="/assets/placeholder.png" />
</IonCol>
</IonRow>
</>
}
</IonContent>
</IonPage>
);
}
export default Search;

View File

@@ -0,0 +1,141 @@
/* 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;
--ion-color-main: #1BAD64;
--ion-color-main-rgb: 27,173,100;
--ion-color-main-contrast: #ffffff;
--ion-color-main-contrast-rgb: 255,255,255;
--ion-color-main-shade: #189858;
--ion-color-main-tint: #32b574;
--ion-background-color: white;
--ion-tab-bar-background: rgb(36, 36, 36);
--ion-toolbar-background: rgb(36, 36, 36);
--ion-tab-bar-color-selected: rgb(27, 173, 100);
--ion-tab-bar-color: rgb(97, 97, 97);
}
.ion-color-main {
--ion-color-base: var(--ion-color-main);
--ion-color-base-rgb: var(--ion-color-main-rgb);
--ion-color-contrast: var(--ion-color-main-contrast);
--ion-color-contrast-rgb: var(--ion-color-main-contrast-rgb);
--ion-color-shade: var(--ion-color-main-shade);
--ion-color-tint: var(--ion-color-main-tint);
}
ion-toolbar ion-title {
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button {
--color: rgb(27, 173, 100);
}
ion-content ion-toolbar ion-title {
color: rgb(36, 36, 36);
}
* {
font-family: 'Ubuntu', sans-serif;
}
ion-toolbar {
--border-style: none;
}
.customLoader {
--background: var(--ion-toolbar-background);
--spinner-color: var(--ion-tab-bar-color-selected);
color: white;
}
.customModal {
--background: var(--ion-toolbar-background);
}
a {
text-decoration: none;
}

View File

@@ -37,7 +37,7 @@ import { useRef } from 'react';
const maptilerProvider = maptiler('d5JQJPLLuap8TkJJlTdJ', 'streets');
const ViewPlace = ({}) => {
const pageRef = useRef();
const pageRef = useRef(null);
const [present, dismiss] = useIonLoading();
const { id } = useParams();
const record = RecordsStore.useState(fetchRecord(id));

View File

@@ -31,8 +31,8 @@ import FinishModal from '../components/FinishModal';
import { useParams } from 'react-router';
const ActiveScoreboard = () => {
const pageRef = useRef();
const headingRef = useRef();
const pageRef = useRef(null);
const headingRef = useRef(null);
const router = useIonRouter();
const { id } = useParams();

View File

@@ -33,7 +33,7 @@ import { getActiveScoreboard } from '../store/Selectors';
import './Page.css';
const Dashboard = () => {
const pageRef = useRef();
const pageRef = useRef(null);
const router = useIonRouter();
const activeScoreboard = useStoreState(MainStore, getActiveScoreboard);

View File

@@ -1,4 +1,21 @@
import { IonButton, IonButtons, IonCard, IonCardContent, IonCardSubtitle, IonCol, IonContent, IonHeader, IonIcon, IonLabel, IonMenuButton, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react';
import {
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCardSubtitle,
IonCol,
IonContent,
IonHeader,
IonIcon,
IonLabel,
IonMenuButton,
IonPage,
IonRow,
IonText,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { arrowForward } from 'ionicons/icons';
import { useStoreState } from 'pullstate';
import { useRef } from 'react';
@@ -8,12 +25,11 @@ import { getScoreboards } from '../store/Selectors';
import './Page.css';
const PreviousScoreboards = () => {
const pageRef = useRef();
const scoreboards = useStoreState(MainStore, getScoreboards)
const pageRef = useRef(null);
const scoreboards = useStoreState(MainStore, getScoreboards);
return (
<IonPage ref={ pageRef }>
<IonPage ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
@@ -30,33 +46,37 @@ const PreviousScoreboards = () => {
</IonToolbar>
</IonHeader>
{ scoreboards.length > 0 &&
{scoreboards.length > 0 && (
<>
{ scoreboards.map((scoreboard, index) => {
{scoreboards.map((scoreboard, index) => {
return (
<IonCard key={ index } className="animate__animated animate__slideInLeft active-scoreboard-card">
<IonCard
key={index}
className="animate__animated animate__slideInLeft active-scoreboard-card"
>
<IonCardContent>
<IonRow>
<IonCol size="6">
<IonCardSubtitle color="light">Title</IonCardSubtitle>
<IonText color="light">
<p className="ion-text-wrap">{ scoreboard.title }</p>
<p className="ion-text-wrap">{scoreboard.title}</p>
</IonText>
</IonCol>
<IonCol size="3" className="ion-text-center">
<IonCardSubtitle color="light">Players</IonCardSubtitle>
<IonText color="light">
<p>{ scoreboard.players && scoreboard.players.length }</p>
<IonText color="light">
<p>{scoreboard.players && scoreboard.players.length}</p>
</IonText>
</IonCol>
<IonCol size="2">
<IonButton color="light" fill="outline" routerLink={ `/page/active-scoreboard/${ scoreboard.id }`}>
<IonIcon icon={ arrowForward } />
<IonButton
color="light"
fill="outline"
routerLink={`/page/active-scoreboard/${scoreboard.id}`}
>
<IonIcon icon={arrowForward} />
</IonButton>
</IonCol>
</IonRow>
@@ -65,19 +85,26 @@ const PreviousScoreboards = () => {
);
})}
</>
}
)}
{ scoreboards.length < 1 &&
{scoreboards.length < 1 && (
<IonRow>
<IonCol size="12" className="ion-text-center">
<IonLabel color="primary">
<h1>No scoreboards to show</h1>
<p>You can easily add a new one</p>
</IonLabel>
<IonButton className="ion-margin-top" color="primary" fill="outline" routerLink="/page/Dashboard">Add one &rarr;</IonButton>
<IonButton
className="ion-margin-top"
color="primary"
fill="outline"
routerLink="/page/Dashboard"
>
Add one &rarr;
</IonButton>
</IonCol>
</IonRow>
}
)}
</IonContent>
</IonPage>
);

View File

@@ -1,4 +1,22 @@
import { IonBackButton, IonBreadcrumb, IonBreadcrumbs, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonSearchbar, IonTitle, IonToolbar, useIonViewWillEnter, useIonViewWillLeave, IonRouterLink, useIonModal } from '@ionic/react';
import {
IonBackButton,
IonBreadcrumb,
IonBreadcrumbs,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonPage,
IonRow,
IonSearchbar,
IonTitle,
IonToolbar,
useIonViewWillEnter,
useIonViewWillLeave,
IonRouterLink,
useIonModal,
} from '@ionic/react';
import { useStoreState } from 'pullstate';
import { useRef } from 'react';
import { useState } from 'react';
@@ -9,49 +27,43 @@ import { ProductStore } from '../store';
import { getCategoryProducts } from '../store/Selectors';
import { capitalizeWords } from '../utils';
import styles from "./Category.module.scss";
import styles from './Category.module.scss';
const Category = () => {
const pageRef = useRef();
const pageRef = useRef(null);
const { name } = useParams();
const products = useStoreState(ProductStore, getCategoryProducts(name));
const [ selectedProduct, setSelectedProduct ] = useState(false);
const handleShowModal = product => {
const [selectedProduct, setSelectedProduct] = useState(false);
const handleShowModal = (product) => {
setSelectedProduct(product);
present({
cssClass: "product-modal",
presentingElement: pageRef.current
cssClass: 'product-modal',
presentingElement: pageRef.current,
});
}
const [ present, dismiss ] = useIonModal(ProductModal, {
};
const [present, dismiss] = useIonModal(ProductModal, {
dismiss: () => dismiss(),
product: selectedProduct
product: selectedProduct,
});
useIonViewWillEnter(() => {
document.querySelector("ion-tab-bar").style.display = "none";
document.querySelector('ion-tab-bar').style.display = 'none';
});
useIonViewWillLeave(() => {
document.querySelector("ion-tab-bar").style.display = "";
})
document.querySelector('ion-tab-bar').style.display = '';
});
return (
<IonPage ref={ pageRef }>
<IonPage ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton />
</IonButtons>
<IonTitle>{ capitalizeWords(name) }</IonTitle>
<IonTitle>{capitalizeWords(name)}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
@@ -60,25 +72,33 @@ const Category = () => {
{/* <IonTitle size="large">{ capitalizeWords(name) }</IonTitle> */}
<IonBreadcrumbs>
<IonBreadcrumb color="dark">
<IonRouterLink routerLink="/home" routerDirection="back" color="dark">Shop</IonRouterLink>
<IonRouterLink routerLink="/home" routerDirection="back" color="dark">
Shop
</IonRouterLink>
</IonBreadcrumb>
<IonBreadcrumb color="primary" active>
{capitalizeWords(name)}
</IonBreadcrumb>
<IonBreadcrumb color="primary" active>{ capitalizeWords(name) }</IonBreadcrumb>
</IonBreadcrumbs>
</IonToolbar>
</IonHeader>
<IonGrid className="ion-padding">
<IonRow className={ styles.searchContainer }>
<IonRow className={styles.searchContainer}>
<IonCol size="12">
<IonSearchbar animated placeholder="Search for a product" />
</IonCol>
</IonRow>
<IonRow>
{ products.map((product, index) => {
return <Product product={ product } key={ `product_${ index }` } click={ () => handleShowModal(product) } />;
{products.map((product, index) => {
return (
<Product
product={product}
key={`product_${index}`}
click={() => handleShowModal(product)}
/>
);
})}
</IonRow>
</IonGrid>

View File

@@ -25,7 +25,7 @@ import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
const Home = () => {
const router = useIonRouter();
const pageRef = useRef();
const pageRef = useRef(null);
const [showModal, setShowModal] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(false);

View File

@@ -1,38 +1,46 @@
import { IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
import {
IonBadge,
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonPage,
IonRow,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { cart, star } from 'ionicons/icons';
import { useRef } from 'react';
import { addToCart } from '../store/CartStore';
import { capitalizeWords } from '../utils';
import styles from "./ProductModal.module.scss";
import styles from './ProductModal.module.scss';
export const ProductModal = ({ dismiss, product }) => {
const cartRef = useRef();
const cartRef = useRef(null);
const handleAddToCart = () => {
cartRef.current.style.display = "inline";
cartRef.current.style.display = 'inline';
addToCart(product);
setTimeout(() => {
cartRef.current.style.display = "none";
cartRef.current.style.display = 'none';
}, 750);
}
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>View Product</IonTitle>
<IonButtons slot="end">
<IonButton onClick={ dismiss }>
Close
</IonButton>
<IonButton onClick={dismiss}>Close</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
@@ -40,27 +48,29 @@ export const ProductModal = ({ dismiss, product }) => {
<IonGrid>
<IonRow className="animate__animated animate__faster animate__slideInUp">
<IonCol size="12">
<IonCard className={ styles.productCard }>
<IonCard className={styles.productCard}>
<IonCardContent>
<div className={ styles.productTopInfo }>
<img src={ product.image } alt="product" />
<div className={ styles.productDetails }>
<IonBadge color="primary">{ capitalizeWords(product.category) }</IonBadge>
<h3>{ product.title }</h3>
<div className={styles.productTopInfo}>
<img src={product.image} alt="product" />
<div className={styles.productDetails}>
<IonBadge color="primary">{capitalizeWords(product.category)}</IonBadge>
<h3>{product.title}</h3>
</div>
</div>
<div className={ styles.productDescription }>
<div className={styles.productDescription}>
<h3>Product Description</h3>
<p>{ product.description }</p>
<p>{product.description}</p>
</div>
<div className={ styles.productDescription }>
<div className={styles.productDescription}>
<h3>Product Rating</h3>
<div>
<p>{ product.rating.count} people have bought this item from the IonShop and have rated an average of { product.rating.rate }.</p>
<IonIcon icon={ star } color="primary" />
<p>
{product.rating.count} people have bought this item from the IonShop and
have rated an average of {product.rating.rate}.
</p>
<IonIcon icon={star} color="primary" />
</div>
</div>
</IonCardContent>
@@ -69,15 +79,22 @@ export const ProductModal = ({ dismiss, product }) => {
</IonRow>
</IonGrid>
<IonGrid className={ `${ styles.bottom } animate__animated animate__slideInUp` }>
<IonGrid className={`${styles.bottom} animate__animated animate__slideInUp`}>
<IonRow>
<IonCol size="12">
<div className={ styles.price }>
<div className={styles.price}>
Buy now for
<IonBadge color="dark" className="ion-padding-left">£{ product.price.toFixed(2) }</IonBadge>
<IonBadge color="dark" className="ion-padding-left">
£{product.price.toFixed(2)}
</IonBadge>
</div>
<IonIcon ref={ cartRef } className={ `${ styles.animatedCart } animate__animated animate__slideInUp` } icon={ cart } color="dark" />
<IonButton onClick={ handleAddToCart }>Add to Cart</IonButton>
<IonIcon
ref={cartRef}
className={`${styles.animatedCart} animate__animated animate__slideInUp`}
icon={cart}
color="dark"
/>
<IonButton onClick={handleAddToCart}>Add to Cart</IonButton>
</IonCol>
</IonRow>
</IonGrid>

View File

@@ -30,7 +30,7 @@ import styles from './Profile.module.scss';
const Profile = () => {
const { id } = useParams();
const router = useIonRouter();
const headingRef = useRef();
const headingRef = useRef(null);
const [slideSpace, setSlideSpace] = useState(0);
const [profile, setProfile] = useState([]);